Repository navigation
fix(android): Prevent text cut-off by clamping lineHeight to font height - #56864
Open
aswinandro wants to merge 1 commit into
Open
aswinandro wants to merge 1 commit into
aswinandro wants to merge 1 commit into
Conversation
On Android 15 & 16, text was being cut off when lineHeight was set smaller than the font’s ascent + descent, due to changes in the platform’s text rendering. This patch updates CustomLineHeightSpan to always ensure the line box is at least as large as the font’s required height, and sets fm.top/fm.bottom to match the adjusted ascent/descent for all lines. This prevents descenders and ascenders from being clipped, matching native TextView and web behavior, and does not affect other text features or layouts. Fixes react#56402, react#53286.
|
Additional reproduction data from Xiaomi devices, for confirmation:
This comment was added with the assistance of an AI coding agent; please treat the data as a supplementary reproduction note. |
40 tasks done
1 of 7 tasks
|
@j-piasecki has imported this pull request. If you are a Meta employee, you can view this in D117161924. |
Contributor
|
This change makes it impossible to set line height lower than the font size. |
meta-codesync Bot
pushed a commit
that referenced
this pull request
Sep 11, 2026
…id 15+ so the last line is not clipped (#58280) Summary: On Android 15+ (API 35), an app that targets API 35+ gets bounds-based line breaking in every `TextView` by default — the platform compat change `TextView#USE_BOUNDS_FOR_WIDTH`: ```java // frameworks/base/core/java/android/widget/TextView.java ChangeId EnabledSince(targetSdkVersion = VERSION_CODES.VANILLA_ICE_CREAM) public static final long USE_BOUNDS_FOR_WIDTH = 63938206; … if (!hasUseBoundForWidthValue) { mUseBoundsForWidth = CompatChanges.isChangeEnabled(USE_BOUNDS_FOR_WIDTH); } ``` React Native measures `<Text>` in `TextLayoutManager` with a `StaticLayout` that breaks lines on glyph **advances** (`buildLayout` never sets `setUseBoundsForWidth`). With `enablePreparedTextLayout` off (the default), the pixels on screen come from `ReactTextView`'s own `TextView` layout — `ReactTextView.setText()` hands the Spannable to `TextView` and `onDraw()` defers to `super.onDraw()`. That layout breaks lines on glyph **bounds**. So measurement and painting disagree on where lines break. For any font whose ink overhangs its advance (script/cursive fonts, several OEM system fonts, emoji fallbacks), a line that fits at measure time can wrap at draw time. The extra line lands outside the Yoga-measured height and is simply never seen: **the last word of a `<Text>` disappears**, while the view is sized as if it were there. This is the mechanism behind #56402 / #53286 (and the shape of #57957: content-sized parent, last line gone). It is independent of `lineHeight`, and it affects both shrink-wrapped single-line text and width-constrained wrapped paragraphs. ## The fix Opt `ReactTextView` out of bounds-based breaking so the drawn layout uses the same advance-based line breaking as measurement. Applied in the constructor and again in `recycleView()` so recycled views cannot drift. The call is resolved reflectively, following the existing `setUseBoundsForWidth` pattern in `TextLayoutManager`, because some internal targets compile against an SDK older than 35 (see `AndroidVersion`). This keeps the final layout on the advance-based behavior React Native has always had — the same principle #57117 states for the layouts it builds — but applies it where the pixels actually come from. It is complementary to #57117: that PR widens the *desired* width for `AT_MOST`/`UNDEFINED` measurement, which does not reach a width-constrained paragraph whose lines are re-broken by the `TextView` at draw time; this change makes both paths agree regardless of constraint mode. Trade-off: React Native forgoes Android 15's automatic reservation of overhang space at the edges of a line (glyph ink may be clipped at the view edge as it was before Android 15). That is the pre-existing behavior on every prior Android version, and strictly better than losing whole words. A follow-up could make *measurement* bounds-aware instead (platform parity), but that changes wrapping app-wide and was the direction of the reverted #54721. Fixes #56402 Related: #53286, #57957, #57117, #56864 ## Changelog: [ANDROID] [FIXED] - Text: the last line no longer disappears on Android 15+ when a font's glyphs overhang their advance (ReactTextView now breaks lines on advances, matching measurement) Pull Request resolved: #58280 Test Plan: ### Deterministic repro (stock emulator, no custom font) API 35/36 AVD, app targeting API 35+. Android's generic `cursive` family (Dancing Script) overhangs heavily. Inside a shrink-wrapping container: ```tsx <View style={{ alignSelf: 'flex-start' }}> <Text style={{ fontFamily: 'cursive', fontSize: 18, lineHeight: 27 }} allowFontScaling={false}> Enjoy your coffee<Text style={{ color: 'green' }}> f</Text> </Text> </View> ``` **Before:** the green `f` is not painted. The view is sized for it (measure), but the `TextView` breaks the line on bounds, wraps the `f` to a second line, and that line is outside the measured height. Which strings trip it depends on where the bounds-based break falls relative to the advance-based one — in the rn-tester example below two of the four cursive rows lose the `f` — while a control row with a non-overhanging font (Roboto) always keeps it. **After:** the `f` is painted on the first line. **Before** (rn-tester `Text` example, API 36 emulator — the cursive column loses its `f` on two of the four rows; the default-font control column keeps every one):  **After** (same example, this branch):  ### rn-tester `Text` → **"Android 15+ glyph overhang (last line must not disappear)"** — the rows above, cursive on the left with a default-font control on the right. Every row must show its green `f`. ### Unit tests `ReactTextViewTest`: - `breaksLinesOnAdvancesLikeMeasurementOnApi35` — a freshly constructed `ReactTextView` reports `useBoundsForWidth == false` on API 35. - `recyclingRestoresAdvanceBasedLineBreaking` — after `useBoundsForWidth = true`, `recycleView()` restores `false`. Below API 35 the reflective lookup returns null and the view is untouched. ### Origin Reported in production by a user on a Samsung SM-A566B (Android 16, One UI system font): trailing words vanished from chat messages while the message bubble was sized for the full text. Pinning a bundled font (Alef) in the app made it stop — consistent with the mechanism above — and the same symptom then reproduced on an AOSP emulator with the `cursive` family as shown here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed By: christophpurrer Differential Revision: D118632937 Pulled By: javache fbshipit-source-id: bb60e5549132c22d635c7ef96a9dba940e61b8e1
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Android 15 & 16, text was being cut off when
lineHeightwas set smaller than the font’s ascent + descent, due to changes in the platform’s text rendering.This patch updates
CustomLineHeightSpanto always ensure the line box is at least as large as the font’s required height, and setsfm.top/fm.bottomto match the adjusted ascent/descent for all lines.This prevents descenders and ascenders from being clipped, matching native TextView and web behavior, and does not affect other text features or layouts.
Fixes #56402, #53286, #56402
Changelog:
[Android] [Fixed] - Prevent text cut-off when
lineHeightis less than font size by clamping line height to font metrics inCustomLineHeightSpan. This ensures text is never clipped on Android 15 & 16 and matches native TextView/web behavior. Fixes #56402, #53286, 56402For more details, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests
Test Plan
fontSizeandlineHeight, including cases wherelineHeight < fontSize.Example test code: